﻿using System;
using System.Collections.Generic;
using System.Linq;
using TSU.FAMC.ComputerEngineering.Classes.Enums;

namespace TSU.FAMC.ComputerEngineering.Classes.BooleanMaths.Fundamental
{
    /// <summary>
    /// 2D matrix of TRUE/FALSE values.
    /// </summary>
    public class BooleanMatrix : ICloneable, IComparable
    {
        /// <summary>
        /// Matrix data.
        /// </summary>
        private BooleanVector _booleanData;

        /// <summary>
        /// Number of matrix rows.
        /// </summary>
        private int _rowsCount;

        /// <summary>
        /// Number of matrix rows.
        /// </summary>
        public int RowsCount
        {
            get
            {
                return _rowsCount;
            }
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException("Rows count cannot be less then 0.");
                }

                _rowsCount = value;
                
                // updating data
                _booleanData.Length = (RowsCount * ColumnsCount);
            }
        }

        /// <summary>
        /// Number of matrix columns.
        /// </summary>
        private int _columnsCount;

        /// <summary>
        /// Number of matrix columns.
        /// </summary>
        public int ColumnsCount
        {
            get
            {
                return _columnsCount;
            }
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException("Columns count cannot be less then 0.");
                }

                _columnsCount = value;

                // updating data
                _booleanData.Length = (RowsCount * ColumnsCount);
            }
        }

        /// <summary>
        /// Default constructor.
        /// </summary>
        public BooleanMatrix()
        {
            _booleanData = new BooleanVector();
        }

        /// <summary>
        /// Copy constructor.
        /// </summary>
        /// <param name="matrix">Boolean matrix object to copy values from.</param>
        public BooleanMatrix(BooleanMatrix matrix)
        {
            // copying values
            _booleanData = new BooleanVector();
            RowsCount = matrix.RowsCount;
            ColumnsCount = matrix.ColumnsCount;
            _booleanData = matrix._booleanData;
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="values">String values of boolean matrix.</param>
        public BooleanMatrix(string values)
        {
            // splitting to rows
            var rowsData = values.Split('\n');

            // deleting empty rows
            var rows = rowsData.Where(t => t != "").ToList();

            // initializing data
            _booleanData = new BooleanVector();

            if (rows.Count == 0)
            {
                // empty matrix
                return;
            }

            // setting dimensions
            RowsCount = rows.Count;
            ColumnsCount = rows[0].Length;

            // populating data
            for (var i = 0; i < RowsCount; i++)
            {
                if (rows[i].Length != ColumnsCount)
                {
                    throw new ArgumentException("Invalid data: rows must have the same length.");
                }

                for (var j = 0; j < ColumnsCount; j++)
                {
                    var value = rows[i][j];
                    if (value != '0' && value != '1')
                    {
                        throw new ArgumentException("Invalid data: acceptable only 1/0 values for boolean matrix.");
                    }

                    _booleanData[i * ColumnsCount + j] = value == '1' ? BooleanValue.True : BooleanValue.False;
                }
            }
        }

        /// <summary>
        /// Copy of matrix rows.
        /// </summary>
        public List<BooleanVector> Rows
        {
            get
            {
                var rows = new List<BooleanVector>();

                // copying rows
                for (var i = 0; i < RowsCount; i++)
                {
                    var row = new BooleanVector(ColumnsCount);
                    for (var j = 0; j < ColumnsCount; j++)
                    {
                        row[j] = this[i, j];
                    }
                    rows.Add(row);
                }

                return rows;
            }
        }

        /// <summary>
        /// Copy of matrix columns.
        /// </summary>
        public List<BooleanVector> Columns
        {
            get
            {
                var columns = new List<BooleanVector>();

                // copying columns
                for (var i = 0; i < ColumnsCount; i++)
                {
                    var column = new BooleanVector(RowsCount);
                    for (var j = 0; j < RowsCount; j++)
                    {
                        column[j] = this[j, i];
                    }
                    columns.Add(column);
                }


                return columns;
            }
        }

        /// <summary>
        /// Matrix indexer.
        /// </summary>
        /// <param name="rowIdx">Row index.</param>
        /// <param name="colIdx">Column index.</param>
        /// <returns>Matrix value by indices.</returns>
        public BooleanValue this[int rowIdx, int colIdx]
        {
            get
            {
                if (rowIdx < 0 || rowIdx >= RowsCount)
                {
                    throw new IndexOutOfRangeException("Invalid row index: use RowsCount property to change number of rows.");
                }

                if (colIdx < 0 || colIdx >= ColumnsCount)
                {
                    throw new IndexOutOfRangeException("Invalid colum index: use ColumnsCount property to change number of columns.");
                }

                return _booleanData[rowIdx * ColumnsCount + colIdx];
            }
            
            set
            {
                if (rowIdx < 0 || rowIdx >= RowsCount)
                {
                    throw new IndexOutOfRangeException("Invalid row index: use RowsCount property to change number of rows.");
                }

                if (colIdx < 0 || colIdx >= ColumnsCount)
                {
                    throw new IndexOutOfRangeException("Invalid colum index: use ColumnsCount property to change number of columns.");
                }

                _booleanData[rowIdx * ColumnsCount + colIdx] = value;
            }
        }

        /// <summary>
        /// Adds new empty row at specified position.
        /// </summary>
        /// <param name="index">Row index.</param>
        public void AddRowAt(int index)
        {
            if (index < 0)
            {
                throw new IndexOutOfRangeException("Invalid row index.");
            }

            if (index >= RowsCount)
            {
                // at the end
                RowsCount = index + 1;
                return;
            }

            RowsCount++;
            
            // shifting rows
            for (var i = RowsCount - 1; i > index; i--)
            {
                for (var j = 0; j < ColumnsCount; j++)
                {
                    this[i, j] = this[i - 1, j];
                }
            }

            // reseting index row
            for (var i = 0; i < ColumnsCount; i++)
            {
                this[index, i] = BooleanValue.False;
            }
        }

        /// <summary>
        /// Adds new empty row at the end of matrix.
        /// </summary>
        public void AddRow()
        {
            AddRowAt(RowsCount);
        }

        /// <summary>
        /// Deletes row from matrix by index.
        /// </summary>
        /// <param name="index">Deleting row index.</param>
        public void DelRowAt(int index)
        {
            if (index < 0 || index >= RowsCount)
            {
                throw new IndexOutOfRangeException("Invalid row index.");
            }

            // shifting rows
            for (var i = index; i < RowsCount - 1; i++)
            {
                for (var j = 0; j < ColumnsCount; j++)
                {
                    this[i, j] = this[i + 1, j];
                }
            }

            // updating counter
            RowsCount--;
        }


        /// <summary>
        /// Adds new empty column at specified position.
        /// </summary>
        /// <param name="index">Column index.</param>
        public void AddColumnAt(int index)
        {
            if (index < 0)
            {
                throw new IndexOutOfRangeException("Invalid column index.");
            }

            BooleanVector newData;

            if (index >= ColumnsCount)
            {
                // at the end

                // building new data
                newData = new BooleanVector(RowsCount * (index + 1));

                // copying data
                for (var i = 0; i < ColumnsCount; i++)
                {
                    for (var j = 0; j < RowsCount; j++)
                    {
                        newData[j * (index + 1) + i] = _booleanData[j * ColumnsCount + i];
                    }
                }

                // updating counter
                ColumnsCount = index + 1;

                // setting data
                _booleanData = newData;

                return;
            }

            // building new data
            newData = new BooleanVector(RowsCount * (ColumnsCount + 1));

            // copying data
            for (var i = 0; i < ColumnsCount + 1; i++)
            {
                if (i == index)
                {
                    continue;
                }

                for (var j = 0; j < RowsCount; j++)
                {
                    var newI = i > index ? i - 1 : i;
                    newData[j * (ColumnsCount + 1) + i] = _booleanData[j * ColumnsCount + newI];
                }
            }

            // updating counter
            ColumnsCount++;

            // setting data
            _booleanData = newData;
        }

        /// <summary>
        /// Adds new empty column at the end of the matrix.
        /// </summary>
        public void AddColumn()
        {
            AddColumnAt(ColumnsCount);
        }

        /// <summary>
        /// Deletes matrix column from specified position.
        /// </summary>
        /// <param name="index"></param>
        public void DelColumnAt(int index)
        {
            if (index < 0 || index >= ColumnsCount)
            {
                throw new IndexOutOfRangeException("Invalid column index.");
            }

            // building new data
            var newData = new BooleanVector(RowsCount * (ColumnsCount - 1));

            // copying data
            for (var i = 0; i < ColumnsCount; i++)
            {
                if (i == index)
                {
                    continue;
                }

                for (var j = 0; j < RowsCount; j++)
                {
                    var newI = i < index ? i : i - 1;
                    var newIndex = j*(ColumnsCount - 1) + newI;
                    var oldIndex = j*ColumnsCount + i;
                    newData[newIndex] = _booleanData[oldIndex];
                }
            }

            // updating counter
            ColumnsCount--;

            // setting data
            _booleanData = newData;
        }

        /// <summary>
        /// Converts matrix to string representation.
        /// </summary>
        /// <returns>String representation of boolean matrix.</returns>
        public override string ToString()
        {
            var result = String.Empty;
            for (var i = 0; i < RowsCount; i++)
            {
                for (var j = 0; j < ColumnsCount; j++)
                {
                    result += this[i, j] == BooleanValue.True ? "1" : "0";
                }

                result += "\n";
            }

            return result;
        }

        /// <summary>
        /// Implicit conversion from BooleanMatrix object to string.
        /// </summary>
        /// <param name="matrix">BooleanMatrix object.</param>
        /// <returns>String representation of the boolean matrix.</returns>
        public static implicit operator string(BooleanMatrix matrix)
        {
            return matrix.ToString();
        }

        /// <summary>
        /// Implicit conversion from string to BooleanMatrix.
        /// </summary>
        /// <param name="values">String values of boolean matrix.</param>
        /// <returns>BooleanMatrix constructed from string values.</returns>
        public static implicit operator BooleanMatrix(string values)
        {
            return new BooleanMatrix(values);
        }

        /// <summary>
        /// Returns integer hash code.
        /// </summary>
        /// <returns>Hash code.</returns>
        public override int GetHashCode()
        {
            return _booleanData.GetHashCode();
        }

        /// <summary>
        /// Compares with arbitrary object.
        /// </summary>
        /// <param name="obj">Arbitrary object to compare matrix with.</param>
        /// <returns></returns>
        public int CompareTo(object obj)
        {
            if (!(obj is BooleanMatrix))
            {
                return Int32.MinValue;
            }

            var matrix = (BooleanMatrix) obj;

            if (ColumnsCount != matrix.ColumnsCount ||
                RowsCount != matrix.RowsCount)
            {
                throw new ArgumentException("Matrix could not be compared because they have different dimensions.");
            }

            return _booleanData.Weight - matrix._booleanData.Weight;
        }

        /// <summary>
        /// Clones boolean matrix object.
        /// </summary>
        /// <returns>Matrix clone.</returns>
        public object Clone()
        {
            var clone = new BooleanMatrix(this);
            return clone;
        }

        /// <summary>
        /// Checks whether boolean matrix equals to arbitrary object.
        /// </summary>
        /// <param name="obj">Object to check equality with.</param>
        /// <returns>True if objects are equal.</returns>
        public override bool Equals(object obj)
        {
            if (obj is BooleanMatrix)
            {
                return Equals(obj as BooleanMatrix);
            }

            return false;
        }

        /// <summary>
        /// Checks whether two boolean matrix values are equal.
        /// </summary>
        /// <param name="obj">Boolean matrix to check values with.</param>
        /// <returns>True if matrix values are equal.</returns>
        public bool Equals(BooleanMatrix obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;

            return _booleanData.Equals(obj._booleanData);
        }

        /// <summary>
        /// Checks value equality of two boolean matrices.
        /// </summary>
        /// <param name="matrix1">First boolean matrix.</param>
        /// <param name="matrix2">Second boolean matrix.</param>
        /// <returns>True if values of two boolean matrices are equal.</returns>
        public static bool operator ==(BooleanMatrix matrix1, BooleanMatrix matrix2)
        {
            if (ReferenceEquals(matrix1, matrix2))
            {
                return true;
            }

            if ((object)matrix1 == null || (object)matrix2 == null)
            {
                return false;
            }

            return matrix1.Equals(matrix2);            
        }

        /// <summary>
        /// Checks value inequality of two boolean matrices.
        /// </summary>
        /// <param name="matrix1">First boolean matrix.</param>
        /// <param name="matrix2">Second boolean matrix.</param>
        /// <returns>True if matrices are not equal.</returns>
        public static bool operator !=(BooleanMatrix matrix1, BooleanMatrix matrix2)
        {
            return !(matrix1 == matrix2);
        }

        /// <summary>
        /// Returns matrix weight.
        /// </summary>
        public int Weight
        {
            get
            {
                return _booleanData.Weight;
            }
        }

        /// <summary>
        /// Returns a weight of the matrix row.
        /// </summary>
        /// <param name="rowIdx">Row index.</param>
        /// <returns>Row's weight.</returns>
        public int RowWeight(int rowIdx)
        {
            if (rowIdx < 0 || rowIdx >= RowsCount)
            {
                throw new IndexOutOfRangeException("Invalid row index");
            }

            var result = 0;
            for (var i = 0; i < ColumnsCount; i++)
            {
                if (this[rowIdx, i] != BooleanValue.True)
                {
                    continue;
                }
                result++;
            }

            return result;
        }

        /// <summary>
        /// Returns a weight of the matrix column.
        /// </summary>
        /// <param name="colIdx">Column index.</param>
        /// <returns>Columns's weight.</returns>
        public int ColumnWeight(int colIdx)
        {
            if (colIdx < 0 || colIdx >= ColumnsCount)
            {
                throw new IndexOutOfRangeException("Invalid column index");
            }

            var result = 0;
            for (var i = 0; i < RowsCount; i++)
            {
                if (this[i, colIdx] != BooleanValue.True)
                {
                    continue;
                }
                result++;
            }

            return result;
        }

    }
}
